/************************************************************************
                                                                     
   Xceed Ultimate ListBox for Silverlight                                                                                                                                            
   Copyright (C) 2010 Xceed Software Inc.    
                                                                     
   This program is provided to you under the terms of the GNU General Public  
   License version 2 as published by the Free Software Foundation. 
        
   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY, without even the implied warranty of 
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.

   You should have received a copy of the GNU General Public License along 
   with this program, if not, write to the Free Software Foundation, Inc., 
   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

   This program can be provided to you by Xceed Software Inc. under a
   proprietary commercial license agreement for use in non-Open Source
   projects. Visit Xceed.com to find the commercial edition of 
   Xceed Ultimate ListBox for Silverlight.                                    
                                                                      
 **********************************************************************/

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using Xceed.Silverlight.Utils;

namespace Xceed.Silverlight.ListBox
{
  internal abstract class TransitionAnimation
  {

    #region Constructor

    internal TransitionAnimation(int duration)
    {
      this.Duration = duration;
    }

    #endregion

    internal event EventHandler<TransitionAnimationEventArgs> Completed;

    #region Duration Property

    internal double Duration
    {
      get;
      set;
    }

    #endregion

    #region HasCompleted Porperty

    internal bool HasCompleted
    {
      get;
      private set;
    }

    #endregion

    #region IsRunning Property

    internal bool IsRunning
    {
      get;
      private set;
    }

    #endregion

    #region StartTime Property

    private int StartTime
    {
      get;
      set;
    }

    #endregion

    #region HandledItems Property

    internal List<ListBoxContainer> HandledItems
    {
      get
      {
        return m_handledItems;
      }
    }

    #endregion

    internal void Initialize( IEnumerable<ListBoxContainer> containers, object state )
    {
      if( containers == null )
        throw new ArgumentNullException( "containers" );

      if( m_initialized )
        throw new InvalidOperationException( "An attempt was made to initialize a TransitionAnimation that is already initialized." );

      m_state = state;

      if( m_initialLayoutInformation != null )
      {
        m_initialLayoutInformation.Clear();
        m_initialLayoutInformation = null;
      }

      m_initialLayoutInformation = TransitionAnimation.CreateLayoutInformation( containers, true );

      //Flag each element of the "old" layout as deferred for size update (avoid too many updates).
      foreach( ListBoxContainer element in m_initialLayoutInformation.Keys )
      {
        element.BeginSizeUpdateDeferring();
      }

      this.HasCompleted = false;
      m_initialized = true;
    }

    internal void StartTransitionAnimation( IEnumerable<ListBoxContainer> containers )
    {
      if( containers == null )
        throw new ArgumentNullException( "containers" );

      if( !m_initialized )
        throw new InvalidOperationException( "An attempt was made to start an uninitialized TransitionAnimation." );

      if( this.IsRunning )
        throw new InvalidOperationException( "An attempt was made to start a TransitionAnimation that has already been started." );

      m_handledItems.Clear();

      Dictionary<ListBoxContainer, ElementLayoutInfo> updatedLayoutInfo = TransitionAnimation.CreateLayoutInformation( containers, false );

      //Flag each element of the "new" layout as deferred for size update (avoid too many updates).
      foreach( ListBoxContainer element in updatedLayoutInfo.Keys )
      {
        element.BeginSizeUpdateDeferring();
      }

      Dictionary<ListBoxContainer, LayoutChangeInfo> changedElements = this.CompareLayouts( m_initialLayoutInformation, updatedLayoutInfo );

      this.DetermineLayoutChangesCore( changedElements );

      foreach( KeyValuePair<ListBoxContainer, LayoutChangeInfo> changeInfo in changedElements )
      {
        //Prepare element for TransitionAnimation
        if( this.PrepareContainer( changeInfo.Key, changeInfo.Value ) )
        {
          //Update the element with start time, ensure that as soon as "start" completes, state if correct for all items.
          if( !this.UpdateContainer( 0d, changeInfo.Key ) )
          {
            //If both preparation let pass the item and update did not filter it out, mark the item as managed by TransitionAnimation
            m_handledItems.Add( changeInfo.Key );
          }
        }
      }

      //deFlag each element of the "new" layout so their size can be updated.
      foreach( ListBoxContainer element in updatedLayoutInfo.Keys )
      {
        element.EndSizeUpdateDeferring();
      }

      //deFlag each element of the "old" layout so their size can be updated.
      foreach( ListBoxContainer element in m_initialLayoutInformation.Keys )
      {
        element.EndSizeUpdateDeferring();
      }

      if( m_handledItems.Count > 0 )
      {
        CompositionTarget.Rendering += OnRendering;
        this.IsRunning = true;

        this.StartTime = Environment.TickCount;
      }
      else
      {
        this.OnCompleted();
      }

    }

    internal abstract void AddContainer( ListBoxContainer container );

    private static Dictionary<ListBoxContainer, ElementLayoutInfo> CreateLayoutInformation( IEnumerable<ListBoxContainer> listBoxElements, bool includeActualAnimationInfo )
    {
      Dictionary<ListBoxContainer, ElementLayoutInfo> dictionary = new Dictionary<ListBoxContainer, ElementLayoutInfo>();

      foreach( ListBoxContainer element in listBoxElements )
      {
        ElementLayoutInfo newInfo = new ElementLayoutInfo();

        Point absoluteElementPosition = element.AbsoluteOffset;


        if( includeActualAnimationInfo )
        {
          absoluteElementPosition = element.CurrentOffset;
          newInfo.Height = element.ActualElementHeight;
          newInfo.Width = element.ActualElementWidth;
          newInfo.Opacity = element.Opacity;
          newInfo.Angle = element.Rotation.Angle;
          newInfo.ScaleFactor = element.Scale.ScaleX;
        }
        else
        {
          absoluteElementPosition = element.AbsoluteOffset;
          newInfo.Height = element.AbsoluteElementHeight;
          newInfo.Width = element.AbsoluteElementWidth;
          newInfo.Opacity = element.AbsoluteOpacity;
          newInfo.Angle = element.AbsoluteRotationAngle;
          newInfo.ScaleFactor = element.AbsoluteScaleFactor;
        }

        newInfo.PositionX = absoluteElementPosition.X;
        newInfo.PositionY = absoluteElementPosition.Y;

        newInfo.MaximumHeight = element.AbsoluteMaximumHeight;
        newInfo.MaximumWidth = element.AbsoluteMaximumWidth;

        dictionary.Add( element, newInfo );
      }

      return dictionary;
    }

    private Dictionary<ListBoxContainer, LayoutChangeInfo> CompareLayouts( Dictionary<ListBoxContainer, ElementLayoutInfo> referenceLayout, Dictionary<ListBoxContainer, ElementLayoutInfo> newLayout )
    {
      Dictionary<ListBoxContainer, LayoutChangeInfo> changesDictionary = new Dictionary<ListBoxContainer, LayoutChangeInfo>();

      List<ListBoxContainer> removedElements = new List<ListBoxContainer>( referenceLayout.Keys );
      List<ListBoxContainer> addedElements = new List<ListBoxContainer>( newLayout.Keys );

      foreach( KeyValuePair<ListBoxContainer, ElementLayoutInfo> newLayoutInfo in newLayout )
      {
        ListBoxContainer key = newLayoutInfo.Key;

        ElementLayoutInfo referenceLayoutInfo;

        if( !referenceLayout.TryGetValue( key, out referenceLayoutInfo ) )
        {
          //if the referenceLayout does not contain the element, its because items was added.
          removedElements.Remove( key );
          continue;
        }

        //If the newLayout's element is present also in referenceLayout, then its not added nor removed.
        addedElements.Remove( key );
        removedElements.Remove( key );

        //perform comparison
        LayoutChangeInfo comparison = this.CompareLayoutInfos( referenceLayoutInfo, newLayoutInfo.Value );

        //If comparison found differences
        if( comparison != null )
        {
          //mark the changes as such
          comparison.ChangeType = ChangeType.ElementChanged;

          //add to dictionary
          changesDictionary.Add( key, comparison );
        }
      }

      //after changes were detected, make sure to add all elements which were removed
      foreach( ListBoxContainer removedElement in removedElements )
      {
        //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
        LayoutChangeInfo layoutChangeInfo = this.CreateLayoutChangeInfo();

        layoutChangeInfo.ChangeType = ChangeType.ElementRemoved;
        layoutChangeInfo.OpacityChange = -removedElement.Opacity;

        changesDictionary.Add( removedElement, layoutChangeInfo );
      }

      //after removed items were added, make sure to add all elements which were added
      foreach( ListBoxContainer addedElement in addedElements )
      {
        //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
        LayoutChangeInfo layoutChangeInfo = this.CreateLayoutChangeInfo();

        layoutChangeInfo.OpacityChange = (addedElement.AbsoluteOpacity);
        layoutChangeInfo.ChangeType = ChangeType.ElementAdded;

        changesDictionary.Add( addedElement, layoutChangeInfo );
      }

      return changesDictionary;
    }

    private LayoutChangeInfo CompareLayoutInfos( ElementLayoutInfo referenceLayoutInfo, ElementLayoutInfo newLayoutInfo )
    {
      //do not create the LayoutChangeInfo directly, allow factory method to do so, to allow derived class customization.
      LayoutChangeInfo changeInfo = this.CreateLayoutChangeInfo();

      if( !LayoutUtils.AreClose( referenceLayoutInfo.PositionX, newLayoutInfo.PositionX ) )
      {
        changeInfo.XChange = referenceLayoutInfo.PositionX - newLayoutInfo.PositionX;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.PositionY, newLayoutInfo.PositionY ) )
      {
        changeInfo.YChange = referenceLayoutInfo.PositionY - newLayoutInfo.PositionY;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Width, newLayoutInfo.Width ) )
      {
        changeInfo.WidthChange = referenceLayoutInfo.Width - newLayoutInfo.Width;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Height, newLayoutInfo.Height ) )
      {
        changeInfo.HeightChange = referenceLayoutInfo.Height - newLayoutInfo.Height;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.MaximumWidth, newLayoutInfo.MaximumWidth ) )
      {
        changeInfo.MaximumWidthChange = referenceLayoutInfo.MaximumWidth - newLayoutInfo.MaximumWidth;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.MaximumHeight, newLayoutInfo.MaximumHeight ) )
      {
        changeInfo.MaximumHeightChange = referenceLayoutInfo.MaximumHeight - newLayoutInfo.MaximumHeight;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Opacity, newLayoutInfo.Opacity ) )
      {
        changeInfo.OpacityChange = referenceLayoutInfo.Opacity - newLayoutInfo.Opacity;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.Angle, newLayoutInfo.Angle ) )
      {
        changeInfo.AngleChange = referenceLayoutInfo.Angle - newLayoutInfo.Angle;
      }

      if( !LayoutUtils.AreClose( referenceLayoutInfo.ScaleFactor, newLayoutInfo.ScaleFactor ) )
      {
        changeInfo.ScaleChange = referenceLayoutInfo.ScaleFactor - newLayoutInfo.ScaleFactor;
      }


      if( changeInfo.IsEmpty )
        return null;

      return changeInfo;
    }

    //This method is there to allow derived TransitionAnimation to subclass and add information to the LayoutChangeInfo class
    //to tailor it to their needs. Standard implementation covers basic translations and resizing scenarios
    internal virtual LayoutChangeInfo CreateLayoutChangeInfo()
    {
      return LayoutChangeInfo.CreateDefaultLayoutChangeInfo();
    }

    //This method provides the default implementation for the calculation of position or size for the 
    //LayoutChangeInfo class. While it is no way mandatory for TransitionAnimations to use this method, it is there
    //for convenience purpose.
    internal static double ComputeLayoutInfoValue( double normalizedElapsedTime, double requiredChange )
    {
      return requiredChange + ( normalizedElapsedTime * -requiredChange );
    }

    //This method, called within the DetermineLayoutChanges allows derived class to see the big picture before deciding how
    //to behave with individual list box elements.
    internal abstract void DetermineLayoutChangesCore( Dictionary<ListBoxContainer, LayoutChangeInfo> changedElements );

    //This method, called within the PrepareContainer method, automatically invoked on each "changed" container detected
    //during the StartTransitionAnimationMethod.
    internal abstract bool PrepareContainerCore( ListBoxContainer container, LayoutChangeInfo changeInfo );

    //This method, called within the OnRendering method, allows derived classes to update the LayoutChangeInfo associated with ListBoxContainer and
    //also allow for "custom" animations and actions to be performed on the ListBoxContainer.
    internal abstract bool UpdateContainerCore( double normalizedElapsedTime, ListBoxContainer container );

    //This method, called within the CleanContainer method, allows derived classes to perform any relevant cleanup operation associated with
    //a ListBoxContainer animation. It is there to "compensate" for any action undertaken in the prepare and update abstract methods.
    internal abstract void CleanContainerCore( ListBoxContainer container );

    internal bool PrepareContainer( ListBoxContainer container, LayoutChangeInfo changeInfo )
    {
      if( this.PrepareContainerCore( container, changeInfo ) )
      {
        return true;
      }

      return false;
    }

    internal bool UpdateContainer( double normalizedElapsedTime, ListBoxContainer container )
    {
      bool retval = this.UpdateContainerCore( normalizedElapsedTime, container );

      container.UpdateLayoutTransform();
      container.UpdateSize();

      return retval;
    }

    private void CleanContainer( ListBoxContainer container )
    {
      //remove hooking done on container for this TransitionAnimation
      container.RemoveAnimation( typeof( PositionAnimation ), this );
      container.RemoveAnimation( typeof( SizeAnimation ), this );
      container.RemoveAnimation( typeof( OpacityAnimation ), this );
      container.RemoveAnimation( typeof( RotateAnimation ), this );
      container.RemoveAnimation( typeof( SizeAnimation ), this );
      container.RemoveAnimation( typeof( ScaleAnimation ), this );

      //Then call core method to allow customized cleaning of the Derived classes.
      this.CleanContainerCore( container );
    }

    private void OnRendering( object sender, EventArgs e )
    {
      //calculate normalized elapsed time 
      int elapsedTime = Environment.TickCount - this.StartTime;
      double normalizedElapsed = ( elapsedTime * 1.0 ) / this.Duration;

      normalizedElapsed = Math.Min( 1d, normalizedElapsed );

      //use array to avoid concurrency update issue.
      ListBoxContainer[] containers = new ListBoxContainer[ m_handledItems.Count ];
      m_handledItems.CopyTo( containers, 0 );

      foreach( ListBoxContainer container in containers )
      {

        if( this.UpdateContainer( normalizedElapsed, container ) )
        {
          this.CleanContainer( container );

          //remove
          m_handledItems.Remove( container );
        }
      }

      //Kill condition for Transition Animation. If it reaches or exceeds 1d normalized elapsed time, then stop everything.
      if( ( normalizedElapsed >= 1d ) && ( m_handledItems.Count > 0 ) )
      {
        //use array to avoid concurrency update issue.
        foreach( ListBoxContainer element in m_handledItems )
        {
          this.CleanContainer( element );
        }

        m_handledItems.Clear();
      }

      //If there are no more animated elements.
      if( m_handledItems.Count == 0 )
      {
        this.OnCompleted();
      }
    }

    private void OnCompleted()
    {
      if( this.IsRunning )
      {
        this.IsRunning = false;
        CompositionTarget.Rendering -= OnRendering;
      }

      this.HasCompleted = true;

      if( this.Completed != null )
      {
        this.Completed( this, new TransitionAnimationEventArgs( m_state ) );
      }

      m_initialized = false;
    }

    #region Private Members

    private object m_state = null;

    private bool m_initialized = false;
    private Dictionary<ListBoxContainer, ElementLayoutInfo> m_initialLayoutInformation;

    private readonly List<ListBoxContainer> m_handledItems = new List<ListBoxContainer>();



    #endregion

    #region Private ElementLayoutInfo Struct

    private struct ElementLayoutInfo
    {
      public double PositionX
      {
        get;
        set;
      }

      public double PositionY
      {
        get;
        set;
      }

      public double Width
      {
        get;
        set;
      }

      public double Height
      {
        get;
        set;
      }

      public double MaximumHeight
      {
        get;
        set;
      }

      public double MaximumWidth
      {
        get;
        set;
      }

      public double Opacity
      {
        get;
        set;
      }

      public double Angle
      {
        get;
        set;
      }

      public double ScaleFactor
      {
        get;
        set;
      }
    } 

    #endregion
   }
}
